from asynchat import async_chat
from protocol import *
import asyncore
import socket
import thread
import time


class Server (object):
    def __init__(self):
        self.nobody_user_num = 1
        self.connected_clients = {}
        self.channels = set()
        thread.start_new_thread(self.ping_connected_clients, ())

    def add_client(self, uid, user):
        self.connected_clients[uid] = user

    def remove_client(self, uid):
        del self.connected_clients[uid]

    def get_client(self, uid):
        return self.connected_clients[uid]

    def get_client_by_name(self, name):
        for client_id, client_object in self.connected_clients.iteritems():
            if client_object.nickname == name:
                return client_object
        return None

    def is_client_connected(self, uid):
        for client_id in self.connected_clients.keys():
            if client_id == uid:
                return True
        return False

    def is_username_connected(self, username):
        if not self.get_client_by_name(username) is None:
            return True
        else:
            return False

    def broadcast_message(self, message):
        for client_id, client in self.connected_clients.iteritems():
            client.client_channel.send_message(message)

    def broadcast_to_channel(self, channel, message):
        for client_id, client in self.connected_clients.iteritems():
            if client.channel == channel:
                client.client_channel.send_message(message)

    def get_clients_in_channel(self, channel):
        channel_clients = []
        for client_id, client in self.connected_clients.iteritems():
            if client.channel == channel:
                channel_clients.append(client)

    def ping_connected_clients(self):
        while True:
            if len(self.connected_clients.keys()) > 0:
                for client in self.connected_clients.itervalues():
                    client.client_channel.send_message(PingCommand(time.time()))
                print "Pinged connected clients (%s)" % len(self.connected_clients.keys())
            time.sleep(10)


class ClientChannel (async_chat):
    def __init__(self, client_info):
        async_chat.__init__(self, client_info[0])
        self.set_terminator("\r\n")
        self.client_info = client_info
        self.user_id = "%s:%s" % client_info[1]
        self.buffer = []
        self.command_response_map = {PongCommand: self.pong_command, NameCommand: self.name_command, WhoCommand: self.who_command, JoinCommand: self.join_command, MsgCommand: self.msg_command}

    def collect_incoming_data(self, incoming_data):
        self.buffer.append(incoming_data)

    def found_terminator(self):
        self.process_data()

    def process_data(self):
        data = "".join(self.buffer)
        self.buffer = []
        try:
            parsed_data = command_parser.parse_string(data)
            target_user = server.get_client(self.user_id)
            if type(parsed_data) in self.command_response_map:
                data_function = self.command_response_map.get(type(parsed_data))
                data_function(parsed_data, target_user)
            else:
                print "Invalid data received from client"
        except ValueError:
            print "Error parsing data from client"

    def pong_command(self, data, user):
        pass

    def name_command(self, data, user):
        data.name = data.name.replace(" ", "")
        print "%s changed their name to %s" % (user.nickname, data.name)
        user.set_nickname(data.name)
        user.client_channel.send_message(NameResponse(data.name))

    def who_command(self, data, user):
        who_users = []
        for u_id, u in server.connected_clients.iteritems():
            who_users.append("%s [%s]" % (u.nickname, u.channel))
        print "Connected users: " + ", ".join(who_users)
        user.client_channel.send_message(WhoResponse(", ".join(who_users)))

    def join_command(self, data, user):
        data.channel = data.channel.upper()
        print "%s joined the channel %s" % (user.nickname, data.channel)
        user.set_channel(data.channel.replace(" ", "").upper())
        if not data.channel in server.channels:
            server.channels.add(data.channel)
        user.client_channel.send_message(JoinResponse(user.nickname, data.channel))
        server.broadcast_to_channel(data.channel, MsgCommand(data.channel, "%s joined your channel" % user.nickname))

    def msg_command(self, data, user):
        target_info = data.target.split(":")
        target_type = target_info[0]
        target_name = target_info[1]
        formatted_message = "[%s] <%s> %s" % (user.channel, user.nickname, data.message)
        print formatted_message
        if target_type == "c":
            server.broadcast_to_channel(target_name, MsgCommand(target_name, formatted_message))
        elif target_type == "u":
            if server.is_username_connected(target_name):
                command_to_send = MsgCommand("u:" + target_name, "@ " + formatted_message)
                print command_to_send
                server.get_client_by_name(target_name).client_channel.send_message(command_to_send)
            else:
                self.send_message(MsgCommand("u:" + user.nickname, "%s is not online" % target_name))
        else:
            raise ValueError("Invalid target type for message")

    def send_message(self, message):
        self.push(str(message) + self.get_terminator())

    def handle_close(self):
        self.client_disconnect()

    def handle_error(self):
        async_chat.handle_error(self)
        print "Socket error"

    def client_disconnect(self):
        user_id = "%s:%s" % self.client_info[1]
        if server.is_client_connected(user_id):
            server.remove_client(user_id)
            print "Client disconnected: %s" % user_id


class ConnectionDispatcher (asyncore.dispatcher):
    def __init__(self, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.bind(("", port))
        print "Server address: %s:%s" % self.socket.getsockname()
        self.listen(5)

    def handle_accept(self):
        client_info = self.accept()
        user_channel = ClientChannel(client_info)
        user_id = "%s:%s" % client_info[1]
        server.add_client(user_id, User(user_id, user_channel))
        print "Client connected: %s" % user_id


class User (object):
    def __init__(self, uid, client_channel, nickname="Nobody", channel="DEFAULT"):
        self.uid = uid
        self.nickname = nickname
        if self.nickname == "Nobody":
            self.nickname += str(server.nobody_user_num)
            server.nobody_user_num += 1
        self.client_channel = client_channel
        self.channel = channel

    def set_nickname(self, nickname):
        self.nickname = nickname

    def set_channel(self, channel):
        self.channel = channel


if __name__ == "__main__":
    address = ("localhost", 0)
    server = Server()
    dis = ConnectionDispatcher(address[1])
    print "Starting server"
    command_parser = CommandParser()
    asyncore.loop()